/*
 * TCSS 558
 * Distributed HashMap
 * Fall 2013
 * James Marquardt
 * Matt Adams
 * Justin Wong
 */
package node;

import static com.google.common.base.Preconditions.checkNotNull;

import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import tools.Log;

import model.Entry;
import model.HostPort;
import model.NodesAndEntries;
import model.RemoteNodeData;
import model.RemoteNodeDataAndEntries;

/**
 * NodeRMIProxy is used to establish a connection to the RMI Endpoint of the node
 * represented by this proxy. 
 * 
 * @author Matt Adams
 */
public class NodeRMIProxy extends Node {
    
    private static final String NAME_IN_REGISTRY = RemoteNodeRMIEndpoint.NAME_IN_REGISTRY;  
    
    /**
     * Remote used to connect to a RemoteNodeRMIEndpoint.Method calls
     * on this RemoteNode will call methods in the corresponding RMI Endpoint for
     * this node.
     */
    private RemoteNode remoteNode;
    
    /**
     * The local host and port.
     */
    private HostPort localHostPort; 
    
    /**
     * The local RMI Endpoint.
     */
    private RemoteNodeRMIEndpoint localRMIEndpoint;
    
    /**
     * Constructs NodeRMIProxy object using RemoteNodeData.
     * 
     * @param nodeData endpoint RemoteNodeData.
     * @param localHostPort Local host and port.
     */
    public NodeRMIProxy(RemoteNodeData nodeData, HostPort localHostPort) {
        this.remoteNode = nodeData.getRemoteNode();
        this.nodeHostPort = nodeData.getHostPort();
        this.nodeID = nodeData.getNodeID();
        this.localHostPort = localHostPort;
        this.localRMIEndpoint = RemoteNodeRMIEndpoint.getRMIEndpoint(this.localHostPort);
    }
    
    /**
     * Constructs NodeRMIProxy object using only host and port information.
     * 
     * @param localHostPort Local host and port.
     * @param endpointHostPort Endpoint host and port.
     * @throws RemoteException RemoteException for registry lookup.
     */
    public NodeRMIProxy(HostPort localHostPort, HostPort endpointHostPort) throws RemoteException, NotBoundException {

        this.nodeHostPort = endpointHostPort;
        this.localHostPort = localHostPort; 

        this.remoteNode = (RemoteNode) LocateRegistry.getRegistry(endpointHostPort.getHost(), 
                                                                  endpointHostPort.getPort()).lookup(NAME_IN_REGISTRY + endpointHostPort.toString());
        this.nodeID = this.remoteNode.getNodeID();
        this.localRMIEndpoint = RemoteNodeRMIEndpoint.getRMIEndpoint(this.localHostPort);
    }
    
    @Override
    public Node findSuccessor(int key) {
        try {
            RemoteNodeData remoteData = this.remoteNode.findSuccessor(key);
            return new NodeRMIProxy(remoteData, this.localHostPort);
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void insertEntry(Entry entry) {
        try {
			this.remoteNode.insertEntry(entry);
		} catch (RemoteException e) {
			throw new RuntimeException(e);
		}
        
    }

    @Override
    public void removeEntry(Entry entry) {
        try {
            this.remoteNode.removeEntry(entry);
        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }       
    }
    
    @Override
    public Entry getEntry(int key) {
    	 try {
 			return this.remoteNode.getEntry(key);
 		} catch (RemoteException e) {
 			throw new RuntimeException(e);
 		}
    }
    
    
    @Override
    public boolean containsEntry(Entry entry) {
        // TODO Auto-generated method stub
        return false;
    }
    
    @Override
    public void disconnect() {
        try {
            this.remoteNode.closeConnections();
        } catch (RemoteException e) {
            Log.err("Failed to close connection to node with ID: " + this.nodeID + ". " + e);
        }
    }

    @Override
    public void leaveNetworkNotify(Node predecessor) {
        RemoteNodeData data = nodeToRemoteNodeData(predecessor);
        try {
            this.remoteNode.leaveNetworkNotify(data);
        } catch (RemoteException e) {
            Log.err("Error in NodeRMIProxy calling leaveNetworkNotify on remoteNode. " + e);
        }       
    }
    
    /**
     * 
     * @return
     */
    public RemoteNode getRemoteNode() {
        return this.remoteNode;
    }
    
    @Override
    public NodesAndEntries notifyWithPotentialPredecessor(Node potentialPredecessor) throws RemoteException {
        RemoteNodeDataAndEntries remoteDataAndEntries = null;

        remoteDataAndEntries = this.remoteNode.notifyWithPotentialPredecessor(nodeToRemoteNodeData(potentialPredecessor));

        final List<Node> nodes = new ArrayList<Node>();
        final List<RemoteNodeData> remoteData = remoteDataAndEntries.getRemoteData();
        for (RemoteNodeData data: remoteData) {
            nodes.add(new NodeRMIProxy(data, this.localHostPort));
        }
        return new NodesAndEntries(nodes, remoteDataAndEntries.getEntries());
    }

    @Override
    public void setSuccessor(Node successorNode) {
        try {
            RemoteNodeData data = null; 
            data = nodeToRemoteNodeData(successorNode);
         
            this.remoteNode.setSuccessor(data);

        } catch (RemoteException e) {
            throw new RuntimeException(e);
        }
        
    }
    
    /**
     * Converts a Node into a RemoteNodeData object for RMI communication.
     * 
     * @param node Node being converted.
     * @return Returns RemoteNodeData object.
     */
    private RemoteNodeData nodeToRemoteNodeData(final Node node) {
        checkNotNull(node, "Node used to build remote data cannot be null.");
        if (node.getNodeHostPort().equals(this.nodeHostPort)) {
          //local HostPort equals the potential predecessor's, can use localRMIEndpoint's remote node.
            return new RemoteNodeData(this.localRMIEndpoint.getRemoteNode(), node.getNodeID(), node.getNodeHostPort());
        } else {
            return new RemoteNodeData(((NodeRMIProxy) node).getRemoteNode(), node.getNodeID(), node.getNodeHostPort());
        }
    }
    
    public Set<Entry> getEntries() {
        RemoteNodeDataAndEntries remoteDataAndEntries = null;
        try {
            remoteDataAndEntries = this.remoteNode.getEntries();
        } catch (RemoteException e) {
            Log.err("Remote Exception getting entries from remote node with ID: " + this.nodeID + ". " + e);
        }
        return remoteDataAndEntries.getEntries();
    }
    
    @Override
	public void addAllEntries(Set<Entry> allEntries) {
        if (allEntries.size() == 0) {
            return;
        }
        try {
            this.remoteNode.addAllEntries(allEntries);
        } catch (RemoteException e) {
            Log.err("Remote Exception getting entries from remote node with ID: " + this.nodeID + ". " + e);
        }	
	}

    @Override
    public void insertCopies(Set<Entry> entryCopiesForInsert) {
        try {
            this.remoteNode.insertCopies(entryCopiesForInsert);
        } catch (RemoteException e) {
            Log.err("Remote Exception inserting entry copies from remote node with ID: " + this.nodeID + ". " + e);
        }       
    }

    @Override
    public void removeCopies(Set<Entry> entryCopiesForRemove) {
        try {
            this.remoteNode.insertCopies(entryCopiesForRemove);
        } catch (RemoteException e) {
            Log.err("Remote Exception removing entry copies from remote node with ID: " + this.nodeID + ". " + e);
        }       
        
    }

    @Override
    public void isLive() throws RemoteException {
        this.remoteNode.isLive();        
    }
}
